﻿/* ------------------------------------------------------------------------ */
/* TheNewCoders Scripting (TNCScripting) v1.0                               */
/* ------------------------------------------------------------------------ */
/* A scripting engine for C# that is even compatible with XNA for Xbox360.  */
/* Copyright (C) 2010 Prageeth Silva <prageeth@thenewcoders.com>            */
/*                                                                          */
/* This program is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU General Public License              */
/* as published by the Free Software Foundation; either version 2           */
/* of the License, or (at your option) any later version.                   */
/*                                                                          */
/* This program is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            */
/* GNU General Public License for more details.                             */
/*                                                                          */
/* You should have received a copy of the GNU General Public License        */
/* along with this program; if not, write to the Free Software              */
/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,               */
/* MA  02110-1301, USA. OR visit <http://www.gnu.org/licenses/gpl-2.0.html> */
/* ------------------------------------------------------------------------ */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace TNCScripting
{

    /// <summary>
    /// Script Helper class
    /// </summary>
    internal class ScriptFunction
    {

        #region "Static/Const"

        private const string Comment = @"//";
        private const string FuncSyntaxErr = "Syntax error in script file {0}; functions should start with the keyword 'function' and end with 'end'.";

        private static char[] seps_line = new char[] { ';' };
        private static char[] seps_comma = new char[] { ',' };

        #endregion

        #region "Regex"

        private static Regex regComments = new Regex(@"\s*//.*");

        private static Regex regFunction = new Regex(@"function\s+\w+\(.*\)", RegexOptions.IgnoreCase);
        private static Regex regFunctionEnd = new Regex(@"^\s*end\s*\Z", RegexOptions.IgnoreCase);
        private static Regex regFunctionRem = new Regex(@"function\s+");

        private static Regex regObjects = new Regex(@"[^.\s]+");
        private static Regex regMethods = new Regex(@"(\w+(?<!if))(\.\w+)*\(.*\)");
        private static Regex regMethodPrms = new Regex(@"\(.*\)");

        private static Regex regReturn = new Regex(@"return\s*", RegexOptions.IgnoreCase);
        private static Regex regValue = new Regex(@"(-?[0-9\.]+)([fFlL])?|(true)|(false)|" + "\\\"(.*)\\\"");
        
        private static Regex regAssign = new Regex(@"[^=]+=[^=]+");
        private static Regex regAssignLeft = new Regex(@"(.*)?=\s*");
        private static Regex regAssignRight = new Regex(@"\s*=(.*)?");

        private static Regex regExpr = new Regex(@"(!)?\w+(.\w+)?(\s*[+-/*%]\s*(!)?\w+(.\w+)?)+");
        private static Regex regExprLeft = new Regex(@"(!)?\w+(.\w+)?\s*[+-/*%]\s*");
        private static Regex regExprRight = new Regex(@"\s*[+-/*%]\s*(!)?\w+(.\w+)?\Z");
        private static Regex regExprOp = new Regex(@"[+-/*%]", RegexOptions.RightToLeft);

        private static Regex regIfCond = new Regex(@"^\s*if\s*\(.+\)\s*\Z", RegexOptions.IgnoreCase);
        private static Regex regIf = new Regex(@"^\s*if\s*", RegexOptions.IgnoreCase);
        private static Regex regElse = new Regex(@"^\s*else\s*\Z", RegexOptions.IgnoreCase);
        private static Regex regEndIf = new Regex(@"^\s*endif\s*\Z", RegexOptions.IgnoreCase);

        private static Regex regComparison = new Regex(@"(!)?\w+(.\w+)?(\s*((\|\|)|&&|==|!=|<=|>=|[<>^])\s*(!)?\w+(.\w+)?)+");
        private static Regex regCompLeft = new Regex(@"(!)?\w+(.\w+)?\s*((\|\|)|&&|==|!=|<=|>=|[<>^])\s*");
        private static Regex regCompRight = new Regex(@"\s*((\|\|)|&&|==|!=|<=|>=|[<>^])\s*(!)?\w+(.\w+)?\Z");
        private static Regex regCompOp = new Regex(@"(\|\|)|&&|==|!=|<=|>=|[<>^]", RegexOptions.RightToLeft);
        private static Regex regCompNot = new Regex(@"!\w+(.\w+)?");

        #endregion

        #region "Variables"

        public Script Script = null;
        public bool Valid = true;
        public string Name = string.Empty;
        public bool Returned = false;
        public object ReturnValue = null;
        public List<string> Args = null;
        public List<Cmd> Commands = null;
        public Dictionary<string, object> Objects = null;

        public Stack<object> Temps = null;
        public Stack<CmdIf> IfCmds = null;

        #endregion

        public ScriptFunction(Script script, List<string> lines)
        {
            this.Script = script;
            this.Objects = new Dictionary<string, object>();
            this.Temps = new Stack<object>();
            this.IfCmds = new Stack<CmdIf>();
            this.Parse(lines);
        }

        public static List<List<string>> GetFuncLines(string[] lines, string file)
        {

            List<string> func = null;
            List<List<string>> list = new List<List<string>>();

            bool f = false;

            foreach (string line in lines)
            {
                string ln = line.Trim();
                if (ln.Length > 0 && !ln.StartsWith(ScriptFunction.Comment))
                {

                    Match m1 = ScriptFunction.regFunction.Match(ln);
                    Match m2 = ScriptFunction.regFunctionEnd.Match(ln);
                    if (!f && m1.Success) // if not a function code, then check if it's a function header
                    {
                        func = new List<string>();
                        func.Add(ln);
                        f = true;
                    }
                    else if (f && m2.Success) // if a function code, then check if it's a function footer
                    {
                        if (func != null)
                        {
                            if (ln.Split(ScriptFunction.seps_line, StringSplitOptions.RemoveEmptyEntries).Length > 1)
                            {
                                throw new Exception(string.Format(ScriptFunction.FuncSyntaxErr, file));
                            }
                            list.Add(func);
                            func = null;
                            f = false;
                        }
                        else
                        {
                            throw new Exception(string.Format(ScriptFunction.FuncSyntaxErr, file));
                        }
                    }
                    else if (f) // A command (but needs to remove any comments at the end of the line)
                    {
                        string[] strs = ln.Split(ScriptFunction.seps_line, StringSplitOptions.RemoveEmptyEntries);
                        foreach (string s in strs)
                        {
                            string str = s.Trim();
                            Match m = ScriptFunction.regComments.Match(str);
                            func.Add(m.Success ? ScriptFunction.RemoveFirst(str, m.Value) : str);
                        }
                    }
                    else
                    {
                        throw new Exception(string.Format(ScriptFunction.FuncSyntaxErr, file));
                    }

                }
            }

            return list;

        }

        private void Parse(List<string> lines)
        {

            if (!this.ProcessHeader(lines[0]))
            {
                this.Valid = false;
                return;
            }

            int tmp = lines.Count - 1;
            if (tmp == 0) { tmp = 1; }

            this.Commands = new List<Cmd>(tmp);
            for (int i = 1; i < lines.Count; i++)
            {
                this.Commands.Add(this.GetCommands(lines[i]));
            }

            if (this.IfCmds.Count > 0) { throw new Exception("Syntax error, 'if' cannot exists without a corresponding 'endif'."); }

        }

        private bool ProcessHeader(string line)
        {

            Match m1 = ScriptFunction.regFunction.Match(line);
            if (m1.Success)
            {

                // remove the function keyword and obtained the other details
                m1 = ScriptFunction.regMethods.Match(m1.Value);

                // get function name
                m1 = ScriptFunction.regMethods.Match(line);
                if (m1.Value.Length > 0)
                {

                    // set the name
                    line = m1.Value.Trim();
                    m1 = ScriptFunction.regMethodPrms.Match(line);
                    this.Name = ScriptFunction.RemoveFirst(line,m1.Value).Trim();

                    // remove the function name
                    line = m1.Value.Trim();

                    if (line.Length > 2)
                    {
                        // remove the parenthese
                        line = line.Substring(1, line.Length - 2);

                        // set the parameters
                        string[] args = line.Split(ScriptFunction.seps_comma, StringSplitOptions.RemoveEmptyEntries);
                        this.Args = new List<string>(args.Length);
                        for (int i = 0; i < args.Length; i++)
                        {
                            this.Args.Add(args[i].Trim());
                        }

                    }

                    return true;

                }
                else
                {
                    return false;
                }

            }

            return false;

        }

        public void InitializeArgs(params object[] args)
        {
            this.Temps.Clear();
            this.Objects.Clear();
            for (int i = 0; i < args.Length; i++)
            {
                this.Objects.Add(this.Args[i], args[i]);
            }
            this.Returned = false;
            this.ReturnValue = null;
        }

        private Cmd GetCommands(string line)
        {

            Cmd cmd = null;

            if (this.GetReturn(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetAssignment(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetIf(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                this.IfCmds.Push(cmd as CmdIf);
                return cmd;
            }
            else if (this.GetElse(line, ref cmd))
            {
                return cmd;
            }
            else if (this.GetEndIf(line, ref cmd))
            {
                this.IfCmds.Pop();
                return cmd;
            }
            else if (this.GetMethod(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetBool(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetProperty(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetExpression(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (this.GetValue(line, ref cmd))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return cmd;
            }
            else if (!line.Contains(' '))
            {
                if (this.IfCmds.Count > 0) { cmd.If = this.IfCmds.Peek().GetParentIf(); }
                return new CmdValue(true, line);
            }

            throw new Exception(string.Format("Syntax error: '{0}'.", line));

        }

        private bool GetAssignment(string line, ref Cmd cmd)
        {
            Match m1 = ScriptFunction.regAssign.Match(line);
            if (m1.Success)
            {
                m1 = ScriptFunction.regAssignRight.Match(line);
                if (m1.Value.Length > 0)
                {
                    MatchCollection mc = ScriptFunction.regObjects.Matches(ScriptFunction.RemoveFirst(line, m1.Value));
                    if (mc.Count == 0) { throw new Exception(string.Format("Syntax error at '{0}'; left operand missing in assignment statement.", line)); }
                    List<string> objs = new List<string>(mc.Count);
                    foreach (Match m in mc)
                    {
                        objs.Add(m.Value);
                    }
                    cmd = new CmdAssign(objs);
                    Match m2 = ScriptFunction.regAssignLeft.Match(line);
                    string args = ScriptFunction.RemoveFirst(line, m2.Value);
                    if (args.Length > 0)
                    {
                        cmd.AddChild(this.GetCommands(args));
                    }
                    return true;
                }
            }
            return false;
        }

        private bool GetIf(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regIfCond.Match(line);
            if (m1.Value.Length > 0)
            {
                Match m2 = ScriptFunction.regIf.Match(m1.Value);
                line = ScriptFunction.RemoveFirst(m1.Value,m2.Value).Trim();
                line = line.Substring(1, line.Length - 2);
                Cmd cmdb = this.GetCommands(line);
                CmdBool cbool = cmdb as CmdBool;
                if (cbool != null)
                {
                    cmd = new CmdIf(cbool);
                    return true;
                }
                else
                {
                    CmdValue cmdv = cmdb as CmdValue;
                    if (cmdv != null && cmdv.Var)
                    {
                        cbool = new CmdBool(cmdv);
                        cmd = new CmdIf(cbool);
                        return true;
                    }
                    else
                    {
                        throw new Exception(string.Format("Syntax error; the if condition '{0}' must result in the boolean type.", line));
                    }
                }
            }
            return false;

        }

        private bool GetElse(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regElse.Match(line);
            if (m1.Value.Length > 0)
            {
                if (this.IfCmds.Count < 1) { throw new Exception("Syntax error, 'else' cannot exists without a corresponding 'if'."); }
                cmd = new CmdElse(this.IfCmds.Peek());
                this.IfCmds.Peek().Else = cmd as CmdElse;
                return true;
            }
            return false;

        }

        private bool GetEndIf(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regEndIf.Match(line);
            if (m1.Value.Length > 0)
            {
                if (this.IfCmds.Count < 1) { throw new Exception("Syntax error, 'endif' cannot exists without a corresponding 'if'."); }
                cmd = new CmdEndIf(this.IfCmds.Peek());
                return true;
            }
            return false;

        }

        private bool GetBool(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regComparison.Match(line);
            if (m1.Success)
            {
                string lft = ScriptFunction.RemoveFirst(m1.Value,ScriptFunction.regCompRight.Match(m1.Value).Value).Trim();
                string rgt = ScriptFunction.RemoveFirst(m1.Value, lft);
                rgt = ScriptFunction.RemoveFirst(rgt, ScriptFunction.regCompOp.Match(m1.Value).Value).Trim();
                string op = ScriptFunction.regCompOp.Match(m1.Value).Value;
                cmd = new CmdBool(op, this.GetCommands(lft), this.GetCommands(rgt));
                return true;
            }
            else
            {
                m1 = ScriptFunction.regCompNot.Match(line);
                if (m1.Success)
                {
                    line = line.Substring(1, line.Length - 1);
                    cmd = new CmdBool("!", this.GetCommands(line));
                    return true;
                }
            }
            return false;

        }

        private bool GetMethod(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regMethods.Match(line);
            if (m1.Value.Length > 0)
            {
                Match m2 = ScriptFunction.regMethodPrms.Match(m1.Value);
                MatchCollection mc = ScriptFunction.regObjects.Matches(ScriptFunction.RemoveFirst(m1.Value,m2.Value));
                List<string> objs = new List<string>(mc.Count);
                foreach (Match m in mc)
                {
                    objs.Add(m.Value.Trim());
                }
                cmd = new CmdMethod(objs);
                string args = m2.Value.Trim();
                if (args.Length > 2)
                {
                    this.GetMethodArgs(args.Substring(1, args.Length - 2), cmd);
                }
                return true;
            }
            return false;

        }

        private int GetMethodArgs(string line, Cmd cmd)
        {
            string[] args = line.Split(ScriptFunction.seps_comma, StringSplitOptions.RemoveEmptyEntries);
            foreach (string s in args)
            {
                cmd.AddChild(this.GetCommands(s.Trim()));
            }
            return args.Length;
        }

        private bool GetProperty(string line, ref Cmd cmd)
        {

            //MatchCollection mc = ScriptFunction.regExpr.Matches(line);
            //if (mc.Count > 0)
            //{
            //    string[] strs = ScriptFunction.regExpr.Split(line);
            //    if (strs.Length > 1 && (strs.Length - 1) == mc.Count)
            //    {
            //        cmd = new CmdExpr(mc[0].Value.Trim());
            //        Cmd cmd2, cmd1 = cmd;
            //        for (int i = 1; i < strs.Length - 1; i++)
            //        {
            //            cmd1.AddChild(this.GetCommands(strs[i].Trim()));
            //            cmd2 = new CmdExpr(mc[i + 1].Value.Trim());
            //            cmd1.AddChild(cmd2);
            //            cmd1 = cmd2;
            //        }
            //        cmd1.AddChild(this.GetCommands(strs[strs.Length - 2].Trim()));
            //        cmd1.AddChild(this.GetCommands(strs[strs.Length - 1].Trim()));
            //        return true;
            //    }
            //}
            return false;

        }

        private bool GetExpression(string line, ref Cmd cmd)
        {

            Match m1 = ScriptFunction.regExpr.Match(line);
            if (m1.Success)
            {
                string lft = ScriptFunction.RemoveFirst(m1.Value, ScriptFunction.regExprRight.Match(m1.Value).Value).Trim();
                string rgt = ScriptFunction.RemoveFirst(m1.Value, lft);
                rgt = ScriptFunction.RemoveFirst(rgt, ScriptFunction.regExprOp.Match(m1.Value).Value).Trim();
                string op = ScriptFunction.regExprOp.Match(m1.Value).Value;
                cmd = new CmdExpr(op, this.GetCommands(lft), this.GetCommands(rgt));
                return true;
            }
            return false;

        }
        
        private bool GetReturn(string line, ref Cmd cmd)
        {
            Match m = ScriptFunction.regReturn.Match(line);

            if (m.Value.Length == 0) { return false; }

            line = ScriptFunction.RemoveFirst(line,m.Value).Trim();
            bool b = line.Length > 0;
            cmd = new CmdReturn(b);
            if (b)
            {
                cmd.AddChild(this.GetCommands(line));
            }
            return true;

        }

        private bool GetValue(string line, ref Cmd cmd)
        {

            Match m = ScriptFunction.regValue.Match(line);

            if (m.Value.Length == 0) { return false; }

            if (m.Value.EndsWith("f") || m.Value.EndsWith("F"))
            {
                float f;
                if (float.TryParse(m.Value.Replace("f", string.Empty).Replace("F", string.Empty), out f))
                {
                    cmd = new CmdValue(f);
                    return true;
                }
            }
            else if (m.Value.Contains("."))
            {
                double d;
                if (double.TryParse(m.Value, out d))
                {
                    cmd = new CmdValue(d);
                    return true;
                }
            }
            else if (m.Value.EndsWith("l") || m.Value.EndsWith("L"))
            {
                long lng;
                if (long.TryParse(m.Value.Replace("l", string.Empty).Replace("L", string.Empty), out lng))
                {
                    cmd = new CmdValue(lng);
                    return true;
                }
            }
            else if (m.Value.Equals("true"))
            {
                cmd = new CmdBool(true, true);
                return true;
            }
            else if (m.Value.Equals("false"))
            {
                cmd = new CmdBool(true, false);
                return true;
            }
            else
            {
                int i;
                long lng;
                if (int.TryParse(m.Value, out i))
                {
                    cmd = new CmdValue(i);
                    return true;
                }
                else if (long.TryParse(m.Value, out lng))
                {
                    cmd = new CmdValue(lng);
                    return true;
                }
                else if (m.Value.StartsWith("\"") && m.Value.EndsWith("\""))
                {
                    cmd = new CmdValue(m.Value.Substring(1, m.Value.Length - 2));
                    return true;
                }
                else
                {
                    cmd = new CmdValue(true, m.Value);
                    return true;
                }
            }

            return false;

        }

        private static string RemoveFirst(string source, string remove)
        {
            return source.Remove(source.IndexOf(remove), remove.Length);
        }

    }

}